﻿// INGENIOUS FRAMEWORK - Semantic Web Development Simplified
// Copyright (C) 2012 Linked Data Tools

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
using System.ComponentModel;
using System.Reflection.Emit;
using System.Threading;
using System.Diagnostics;

using MongoDB.Driver;
using MongoDB.Bson;

using Ingenious.GraphStore;
using Ingenious.Core;

namespace Ingenious.Graph
{
    /// <summary>
    /// Proxy keeps a client-side domain model up to date with the latest changes on a
    /// primary graph store, and handles change propagation and transactions on the client model
    /// </summary>
    public class GraphProxy<TTripleId> : IDisposable
    {
        #region Supporting Classes
        protected class TypePropertyInfo
        {
            private PropertyDescriptor m_propertyDescriptor;
            private bool m_isDelayLoaded;

            #region Object Lifetime
            public TypePropertyInfo(PropertyDescriptor propertyDescriptor, bool isDelayLoaded)
            {
                if (propertyDescriptor == null)
                {
                    throw new ArgumentNullException("propertyInfo");
                }

                m_propertyDescriptor = propertyDescriptor;
                m_isDelayLoaded = isDelayLoaded;
            }
            #endregion

            #region Properties
            public PropertyDescriptor PropertyDescriptor
            {
                get { return m_propertyDescriptor; }
            }

            public bool IsDelayLoaded
            {
                get { return m_isDelayLoaded; }
            }
            #endregion
        }

        protected class TypeFieldInfo
        {
            private FieldInfo m_fieldInfo;

            #region Object Lifetime
            public TypeFieldInfo(FieldInfo fieldInfo)
            {
                if (fieldInfo == null)
                {
                    throw new ArgumentNullException("fieldInfo");
                }

                m_fieldInfo = fieldInfo;
            }
            #endregion

            #region Properties
            public FieldInfo FieldInfo
            {
                get { return m_fieldInfo; }
            }
            #endregion
        }
        #endregion

        // The object property info cache
        private readonly Dictionary<Type, IList<TypePropertyInfo>> m_objectPropertyInfoByType = new Dictionary<Type, IList<TypePropertyInfo>>();
        private GraphStoreBase<TTripleId> m_graphStore;

        private static HashSet<string> s_nodeCorePropertyNames = new HashSet<string>();

        // Cache of open transaction scopes, for automatically passing into entities created
        // within the scope of the transaction
        private static Dictionary<int, List<GraphScope<TTripleId>>> s_transactionScopesByThreadId = new Dictionary<int, List<GraphScope<TTripleId>>>();
        private static object s_transactionLock = new object();

        private readonly FetchMode m_implicitFetchMode;

        #region Object Lifetime
        static GraphProxy()
        {
            Type coreEntityType = typeof(Node<>);

            // Register core properties for ignorance by the provider
            // object serializers
            PropertyDescriptorCollection propertyCollection = TypeDescriptor.GetProperties(coreEntityType);

            foreach (PropertyDescriptor propertyDescriptor in propertyCollection)
            {
                s_nodeCorePropertyNames.Add(propertyDescriptor.Name);
            }
        }

        /// <summary>
        /// Default graph proxy constructor
        /// 
        /// Adopts the synchronization context of the thread in which the constructor
        /// is called.
        /// </summary>
        public GraphProxy() : this(FetchMode.Asynchronous)
        {
        }

        /// <summary>
        /// Graph proxy constructor
        /// 
        /// Adopts the synchronization context of the thread in which the constructor
        /// is called and allows specification of the default (implicit) fetch mode.
        /// </summary>
        public GraphProxy(FetchMode implicitFetchMode)
            : this(implicitFetchMode, SynchronizationContext.Current)
        {
        }

        /// <summary>
        /// Graph proxy constructor
        /// 
        /// Allows explicit setting of the synchronization context you wish to informed
        /// of collection and property changes in graph nodes.
        /// </summary>
        public GraphProxy(FetchMode implicitFetchMode, SynchronizationContext synchronizationContext)
        {
            if (synchronizationContext == null)
            {
                throw new ArgumentNullException("synchronizationContext");
            }

            SynchronizationContext = synchronizationContext;
        }

        public virtual void Dispose()
        {
            if (m_graphStore != null)
            {
                m_graphStore.Dispose();
            }

            m_objectPropertyInfoByType.Clear();
        }
        #endregion

        #region Reflection Property Cache
        //protected virtual void PostRegisterType(Type entityType, List<TypePropertyInfo> typePropertyInfos, List<TypeFieldInfo> typeFieldInfos)
        //{
        //    // Default: Do nothing
        //}

        //public void RegisterType(Entity<TObjectId> entity)
        //{            
        //    dynamic proxy = new Entity<TObjectId>(;

        //    if (!m_objectPropertyInfoByType.ContainsKey(entity.GetType()))
        //    {
        //        // Create cached list of type property descriptions and aggregate
        //        // properties for type efficiency. We use the TypeDescriptor framework
        //        // to allow provider developers to override type behaviour at runtime
        //        PropertyDescriptorCollection propertyDescriptorCollection = TypeDescriptor.GetProperties(proxy.GetType());

        //        List<TypeFieldInfo> typeFieldInfos = new List<TypeFieldInfo>();
        //        FieldInfo[] fis = proxy.GetType().GetFields();

        //        foreach (FieldInfo fi in fis)
        //        {
        //            if (fi.MemberType == MemberTypes.Field &&
        //                fi.IsPublic && !fi.IsStatic)
        //            {
        //                typeFieldInfos.Add(new TypeFieldInfo(fi));
        //            }
        //        }

        //        List<TypePropertyInfo> typePropertyInfos = new List<TypePropertyInfo>();

        //        foreach (PropertyDescriptor propertyDescriptor in propertyDescriptorCollection)
        //        {
        //            // Filter the base class properties, which are transparently ignored
        //            if (!s_entityCorePropertyNames.Contains(propertyDescriptor.Name))
        //            {
        //                bool isDelayLoaded = false;

        //                if (typeof(Entity<TObjectId>).IsAssignableFrom(propertyDescriptor.PropertyType))
        //                {
        //                    // By convention, entity properties are delay loaded, value properties are not
        //                    isDelayLoaded = true;
        //                }

        //                typePropertyInfos.Add(new TypePropertyInfo(propertyDescriptor, isDelayLoaded));
        //            }
        //        }

        //        // Carry out any custom duties after registering the type
        //        // For example, any provider specific serialization type mapping
        //        PostRegisterType(proxy.GetType(), typePropertyInfos, typeFieldInfos);
        //    }
        //}
        #endregion

        #region Server Side Change Detection
        ///
        /// Note: Simply query for all objects with update ID greater than the last
        /// poll time. Then start a client side transaction scope on all those objects
        /// and write the changes, if they have already been pulled that is.
        /// 
        #endregion

        #region Client Side Change Detection
        ///
        /// Note: Simply check that the ID is registered for a transaction, and if so
        /// go ahead and process the change into the change queue against that transaction ID
        /// Changes should be 'summed' for a the cleanest result before submitting to Mongo
        /// 
        #endregion
        
        #region Transaction Handling
        public bool HasOpenTransaction(int threadId, out GraphScope<TTripleId> graphScope)
        {
            graphScope = null;

            lock (s_transactionLock)
            {
                if (s_transactionScopesByThreadId.ContainsKey(threadId))
                {
                    graphScope = s_transactionScopesByThreadId[threadId][0];
                    return true;
                }
            }

            return false;
        }

        public bool HasOpenWriteGraphScope(int threadId, out GraphScope<TTripleId> graphScope)
        {
            graphScope = null;

            lock (s_transactionLock)
            {
                if (s_transactionScopesByThreadId.ContainsKey(threadId))
                {
                    graphScope = s_transactionScopesByThreadId[threadId][0] as GraphScope<TTripleId>;
                    return graphScope.GraphLock == GraphLock.Write;
                }
            }

            return false;
        }

        ///
        /// As this is an open object graph, we can only lock across arbitrary IDs. However,
        /// if we have referenced other entity objects in the form of properties, or alternatively
        /// we have a list item with children, we can opt to lock all of those also up to a
        /// level limit. An entity item as a property is treated no differently to an item in a
        /// child entity list.
        ///
        public void StartTransaction(GraphScope<TTripleId> parentGraphScope, Graph<TTripleId> graph)
        {
            lock (s_transactionLock)
            {
                if (!s_transactionScopesByThreadId.ContainsKey(Thread.CurrentThread.ManagedThreadId))
                {
                    s_transactionScopesByThreadId[Thread.CurrentThread.ManagedThreadId] = new List<GraphScope<TTripleId>>();
                }
                else
                {
                    if (s_transactionScopesByThreadId[Thread.CurrentThread.ManagedThreadId].Contains(parentGraphScope))
                    {
                        throw new TransactionException("Transaction is already started");
                    }
                }

                // Register the transaction for this thread
                // The closest parent scope is always the first in the list
                s_transactionScopesByThreadId[Thread.CurrentThread.ManagedThreadId].Insert(0, parentGraphScope);
            }
        }

        // TODO: Need to 'roll back' the changes if one of the changes fails
        // TODO: Consolidate deltas into net result before submitting to data store
        public void EndTransaction(GraphScope<TTripleId> parentGraphScope, IEnumerable<Triple<TTripleId>> insertChanges, IEnumerable<Triple<TTripleId>> updateChanges, IEnumerable<Triple<TTripleId>> deleteChanges)
        {
            // For write transaction scopes, we need to commit model changes within the scope
            if (parentGraphScope.GraphLock == GraphLock.Write)
            {
                m_graphStore.InsertChangeBatch(parentGraphScope, insertChanges);
                m_graphStore.UpdateChangeBatch(parentGraphScope, updateChanges);
                m_graphStore.RemoveChangeBatch(parentGraphScope, deleteChanges);
            }

            // Now remove the transaction from the registry
            lock (s_transactionLock)
            {
                s_transactionScopesByThreadId[Thread.CurrentThread.ManagedThreadId].Remove(parentGraphScope);

                // Remove the list for the thread if no more transactions open
                if (s_transactionScopesByThreadId[Thread.CurrentThread.ManagedThreadId].Count == 0)
                {
                    s_transactionScopesByThreadId.Remove(Thread.CurrentThread.ManagedThreadId);
                }
            }
        }
        #endregion

        #region Connection To Graph Store
        public void Connect(GraphStoreConnectionInfoBase graphStoreConnectionInfo)
        {
            if (graphStoreConnectionInfo == null)
            {
                throw new ArgumentNullException("graphStoreConnectionInfo");
            }

            ConstructorInfo ci = graphStoreConnectionInfo.GraphStoreType.GetConstructor(new Type[] { GetType(), graphStoreConnectionInfo.GetType() });

            if (ci == null)
            {
                throw new InvalidOperationException("Cannot find graph store matching graph store connection type '" + graphStoreConnectionInfo.GetType().ToString() + "'");
            }

            m_graphStore = (GraphStoreBase<TTripleId>)ci.Invoke(new object[] { this, graphStoreConnectionInfo });
        }

        public void Disconnect()
        {

        }
        #endregion

        #region Provider Updates
        public void InsertChangeBatch(GraphScope<TTripleId> graphScope, HashSet<Triple<TTripleId>> insertChanges)
        {
            m_graphStore.InsertChangeBatch(graphScope, insertChanges);
        }

        public void DeleteChangeBatch(GraphScope<TTripleId> graphScope, HashSet<Triple<TTripleId>> deleteChanges)
        {
            m_graphStore.RemoveChangeBatch(graphScope, deleteChanges);
        }
        #endregion

        #region Properties
        public GraphStoreBase<TTripleId> GraphStore
        {
            get { return m_graphStore; }
            set
            {
                if (m_graphStore != null)
                {
                    throw new ProxyException("Graph store has already been set");
                }

                m_graphStore = value;
            }
        }

        public FetchMode ImplicitFetchMode
        {
            get { return m_implicitFetchMode; }
        }

        public SynchronizationContext SynchronizationContext
        {
            get;
            protected set;
        }
        #endregion
    }
}
